Ownership

Ownership

Suppose a string x declared in main that is used as an argument for a function helper(x : String).

On Rust’s Memory Management:

Example: Ownership v.s. Reference

Let’s look at this Rust code:

fn main() {
    let x = format!("Hello World");
    helper(x);
    helper(x); 
}
fn helper(x : String) {
    println!(x);
}

This code cannot compile because when we call helper the first time, we give ownership of x to it; and so when helper returns, it drops x. As a result, because we try to use x on the second helper(x), we get error[E0382]: use of moved value


Let’s look at this Java code:

void main() {
    Vector x = ...;
    helper(x);
    helper(x); 
}
void helper(Vector x) {
    println!(x);
}

In Java, we give access (references) to things rather than ownership, which entangles their states. When helper modifies x in Java, it’s immediately visible to main.

Moreover, we can never know when to free memory; even if main exits, due to the possibility of helper(x) making threads.

As a result of everything being implicitly shared, data races are a common issue. And since we can’t know when things will be freeable until runtime, we just run the garbage collector when we need more memory, which pauses execution and searches for stuff to free.

Three Categories of Values

Non-Copyable: Values move from place to place.

Clone: Run custom code to copy a value.

Example: Cloning
fn main() {
    let x = format!("Hello World");
    helper(x.clone());
    helper(x); 
}

Copy: Some basic data types are automatically cloned when you use them, as it’s very cheap.

References

Compile-Time Read-Write-Lock

You can never have a reader/writer at the same time.

  1. Creating a shared reference to x “read locks” x.
  2. Creating a mutable reference to x “writes locks” x.

Both locks last until their reference goes out of scope.

Shared Borrows

Shared Borrow (&x): We can borrow variables to create a reference.

More on Borrowed == Immutable:

Mutation is allowed:

  1. In controlled scenarios with specific APIs (e.g., mutex), or
  2. When a mut value is shared borrowed, it can’t be mutated during the borrow—but regains mutability afterward. (see “Example: Immutability of Shared Borrows”)
Example: Shared Borrow

As seen in the previous example, this code doesn’t compile.

fn main() {
    let x = format!("Hello World");
    helper(x);
    helper(x); 
}
fn helper(x : String) {
    println!(x);
}

In this fix, we borrow the string to create a reference and update helper to take a reference to a string.

fn main() {
    let x = format!("Hello World");
    let r = &x;
    helper(r);
    helper(r); 
}
fn helper(x : &String) {
    println!(x);
}
Example: Immutability of Shared Borrows

This is the code from the previous example, with a key change: We made made x mutable, and even push a character to it.

Shouldn’t this not compile because shared referrences are immutable?

fn main() {
    let mut x = format!("Hello World");
    x.push("a");
    let r = &x;
    helper(r);
    helper(r); 
}
fn helper(x : &String) {
    println!("{}", x);
}

The reason the code compiles is that Rust sees x as mutable (as we declared it), until it gets borrowed.

// Does not compile (borrowed == immutable)
fn main() {
    let mut x = format!("Hello World");
    let r = &x;
    helper(r);
    helper(r); 
    x.push("a");
}
// Does compile
fn main() {
    let mut x = format!("Hello World");
    {
        let r = &x;
        helper(r);
        helper(r); 
    }
    x.push("a");
}

Mutable Borrows

Mutable Borrows (&mut x):

Example: Using a Mutable Borrow

This Rust code uses borrowing to concatenate two strings:

pub fn main() {
    let (mut str1, str2) = two_words();
    str1 = join_words(str1, str2);
    println!("concatenated string is {:?}", str1);
}
fn two_words() -> (String, String) {
    (format!("fellow"), format!("Rustaceans"))
}
fn join_words(mut prefix: String, suffix: String) -> String {
    prefix.push(' ');
    for ch in suffix.chars() {
        prefix.push(ch);
    }
    prefix
}

This is a modification of the code to use borrowing and modify str1 in place:

pub fn main() {
    let (mut str1, str2) = two_words();
    join_words(&mut str1, &str2);
    println!("concatenated string is {:?}", str1);
}
fn two_words() -> (String, String) {
    (format!("fellow"), format!("Rustaceans"))
}
fn join_words(prefix: &mut String, suffix: &String) {
    prefix.push(' ');
    for ch in suffix.chars() {
        prefix.push(ch);
    }
}